MoonBit支持实时可视化编程,唤起你的童年记忆「俄罗斯方块」
接下来,我们将分享如何用 MoonBit 来编写俄罗斯方块?
储存整个游戏的状态
首先,我们需要使用 struct Tetris 来储存整个游戏的状态:
struct Tetris {
mut dead:Bool
mut grid:List[Array[Int]]
mut piece_pool:List[PIECE]
mut current:PIECE
mut piece_x:Int
mut piece_y:Int
mut piece_shap:Array[Array[Int]]
mut score:Int
mut row_completed:Int
}
grid
用来保存一个画面里面每个块的颜色,比如:
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0 0
可以用来表示下图:
生成俄罗斯方块
第一步:使用 generate_piece
生成俄罗斯方块
pub func generate_piece(self:Tetris) -> Bool {
self.current = self.get_next_piece(true)
self.piece_shap = self.current.piece_shap()
self.piece_x = grid_col_count/2 - self.piece_shap[0].length()/2
self.piece_y = 0
return check_collision(self.grid, self.piece_shap, (self.piece_x, self.piece_y))
}
先通过 get_next_piece()
来获取下一个方块,就是从 piece_pool
中取出下一个 piece,这里获取的只是一个枚举类型。
pub func get_next_piece(self:Tetris, pop:Bool) -> PIECE {
if self.piece_pool.length() == 0 {
self.generate_piece_pool()
}
let Cons(cur, n) = self.piece_pool
if pop {
self.piece_pool = n
}
cur
}
第二步:通过 piece_shape
来获取具体的形状。俄罗斯方块每种类型的方块表示是用一个二维数组来表示,数组里的值是颜色的索引:
pub func piece_shape(self:PIECE) -> Array[Array[Int]] {
match self {
I => [[1, 1, 1, 1]]
L => [[0, 0, 2],
[2, 2, 2]]
J => [[3, 0, 0],
[3, 3, 3]]
S => [[0, 4, 4],
[4, 4, 0]]
Z => [[5, 5, 0],
[0, 5, 5]]
T => [[6, 6, 6],
[0, 6, 0]]
O => [[7, 7],
[7, 7]]
}
}
比如 L 表示的就是 L 形的图形, 如下图:
第三步:计算 piece 的 x 坐标和 y 坐标
第四步:调用 check_collision
检查是否有冲突
控制俄罗斯方块
我们通过 step 这个函数来移动和旋转方块,根据 action 的值来进行不同的操作:
pub func step(tetris:Tetris, action:Int) {
if tetris.dead {
return
}
match action {
//move left
1 => tetris.move_piece(-1)
//move right
2 => tetris.move_piece(1)
//rotate
3 => tetris.rotate_piece()
//instant
4 => tetris.drop_piece(true)
_ => ()
}
tetris.drop_piece(false)
}
移动俄罗斯方块:
pub func move_piece(self:Tetris, delta:Int) {
var new_x = self.pice_x + delta
new_x = max(0, min(new_x, (grid_col_count - self.pice_shap[0].length())))
if check_collision(self.grid, self.pice_shap, (new_x, self.pice_y)) {
return
}
self.pice_x = new_x
}
旋转俄罗斯方块:
pub func rotate_piece(self:Tetris) {
let r = self.pice_shape.length()
let c = self.pice_shape[0].length()
let new_shape = Array::make(c, Array::make(r, 0))
var i = 0
while i<c {
new_shape[i] = Array::make(r, 0)
i = i+1
}
var i_c = 0
while i_c < c {
var i_r = 0
while i_r < r {
new_shape[i_c][i_r] = self.pice_shape[r-i_r-1][i_c]
i_r = i_r + 1
}
i_c = i_c + 1
}
var new_x = self.pice_x
if (new_x + new_shape[0].length()) > grid_col_count {
new_x = grid_col_count - new_shap[0].length()
}
if check_collision(self.grid, new_shape, (new_x, self.pice_y)) {
return
}
self.piece_x = new_x
self.piece_shape = new_shape
}
掉落俄罗斯方块:
pub func drop_piece(self:Tetris, instant:Bool) {
if instant {
let y = get_effective_height(self.grid, self.pice_shape, (self.piece_x, self.piece_y))
self.piece_y = y + 1
} else {
self.piece_y = self.piece_y + 1
}
if instant == false && check_collision(self.grid, self.pice_shape, (self.piece_x, self.piece_y)) == false {
return
}
self.on_piece_collision()
}
这里的instant
参数用来判断是否是快速掉落方块;使用 on_piece_collison()
查找完整的行。然后消除他们。
消除方块
当一行满的时候方块需要被消除,我们通过 on_piece_collision
来完成消除。
先将这个方块添加进去:
pub func on_piece_collision(self:Tetris) {
// ...
//Add the current shap to grid
fn go1(l:List[Array[Int]], r:Int) {
match l {
Cons(v, n) => {
if r < y {
return go1(n, r + 1)
}
if r >= (y + len_r) {
return
}
var c = 0
while c < len_c {
if self.pice_shap[r - y][c] == 0 {
c = c + 1
continue
}
v[c + self.piece_x] = self.piece_shape[r - y][c]
c = c + 1
}
return go1(n, r + 1)
}
Nil => ()
}
}
go1(self.grid, 0)
}
消除已经填满的行:
pub func on_piece_collision(self : Tetris) {
//...
//Delete the complete row
self.row_completed = 0
fn go2(l:List[Array[Int]]) -> List[Array[Int]] {
match l {
Nil => Nil
Cons(v, n) => {
if contain(v, 0) {
return Cons(v, go2(n))
} else {
self.row_completed = self.row_completed + 1
return go2(n)
}
}
}
}
var new_grid:List[Array[Int]] = Nil
new_grid = go2(self.grid)
}
使用 Moonbit External Ref 画图
根据 Tetris
中保存的信息调用 Canvas 去画图:
pub func draw(canvas : Canvas_ctx, tetris : Tetris) {
var c = 0
//draw backgroud
while c < grid_col_count {
let color = if (c%2) == 0 {0} else {1}
canvas.set_fill_style(color)
canvas.fill_rect(c, 0, 1, grid_row_count)
c = c + 1
}
draw_piece(canvas, tetris.grid, (0, 0))
draw_piece(canvas, tetris.piece_shape.stream(), (tetris.piece_x, tetris.piece_y))
if tetris.dead {
canvas.draw_game_over()
}
}
func draw_piece(canvas:Canvas_ctx, matrix:List[Array[Int]], offset:(Int, Int)) {
fn go(l:List[Array[Int]], r:Int, canvas:Canvas_ctx) {
match l {
Cons(v, n) => {
var c = 0
while c < v.length() {
if v[c] == 0 {
c = c+1
continue
}
canvas.set_fill_style(v[c]+1)
canvas.fill_rect(offset.0 + c, offset.1 + r, 1, 1)
canvas.set_stroke_color(0)
canvas.set_line_width(0.1)
canvas.stroke_rect(offset.0 + c, offset.1 + r, 1, 1)
c = c + 1
}
go(n, r+1, canvas)
}
Nil => ()
}
}
go(matrix, 0, canvas)
}
JavaScript 监听和渲染
添加对键盘事件的监听:
window.addEventListener("keydown", (e) => {
if (!requestAnimationFrameId) return
switch (e.key) {
case "ArrowLeft": {
tetris_step(tetris, 1)
break
}
case "ArrowRight": {
tetris_step(tetris, 2)
break
}
case "ArrowDown": {
tetris_step(tetris, 4)
break
}
case "ArrowUp": {
tetris_step(tetris, 3)
break
}
}
})
draw
(tetris_draw)函数:function update(time = 0) {
const deltaTime = time - lastTime
dropCounter += deltaTime
if (dropCounter > dropInterval) {
tetris_step(tetris, 0);
scoreDom.innerHTML = "score: " + tetris_score(tetris)
dropCounter = 0
}
lastTime = time
tetris_draw(context, tetris);
requestAnimationFrameId = requestAnimationFrame(update)
}
完整的代码:
https://github.com/moonbitlang/moonbit-docs/tree/main/examples/tetris
如果你想要看视频版的教程,可以复制下方链接,也可以在B站搜索「MoonBit月兔」,前往B站观看完整版的视频教程哦~链接:
https://www.bilibili.com/video/BV1HN411E72V/?spm_id_from=333.999.0.0&vd_source=33c2b6daf1758b4a27701c07755543cf
当然,大家如果想体验一下实时coding调试俄罗斯方块,可以直接访问我们在线IDE:https://www.moonbitlang.cn/gallery/tetris/
我们也期待你积极地分享你的编程实践!让我们一起开启MoonBit编程之旅🎉
官方平台账号,欢迎扫码关注
MoonBit
知乎|@张宏波 / @MoonBit
Twitter丨@Moonbitlang
Bilibili丨MoonBit月兔
MoonBit用户交流群|添加小助手rael_helper